🔀 Costrutti di Selezione in C

Guida Completa e Professionale: if, else, switch e Pattern Avanzati

📚 Introduzione: Il Potere delle Decisioni nel Codice

I programmi di computer non sono semplicemente sequenze lineari di istruzioni che vengono eseguite dall'inizio alla fine. La vera potenza della programmazione risiede nella capacità del codice di prendere decisioni, di scegliere percorsi diversi in base a condizioni che possono cambiare durante l'esecuzione. È questa capacità di adattarsi dinamicamente che trasforma una sequenza statica di istruzioni in un programma intelligente e reattivo.

Immagina di dover scrivere un programma che determina se uno studente ha superato un esame. Senza costrutti di selezione, il programma potrebbe solo eseguire sempre le stesse operazioni, indipendentemente dal voto effettivo. Con i costrutti di selezione, invece, il programma può analizzare il voto e decidere: "Se il voto è maggiore o uguale a 18, lo studente ha passato; altrimenti, non ha passato". Questa semplice capacità decisionale è fondamentale.

I costrutti di selezione (anche chiamati costrutti condizionali o statement di controllo del flusso) permettono al programma di eseguire blocchi di codice diversi in base alla valutazione di condizioni logiche. In C, abbiamo a disposizione diversi strumenti per implementare la selezione, ognuno con caratteristiche e casi d'uso specifici:

  • if: esegue un blocco di codice solo se una condizione è vera
  • if-else: sceglie tra due alternative mutuamente esclusive
  • else-if: gestisce multiple condizioni in sequenza
  • switch-case: selezione multipla basata sul valore di un'espressione
  • Operatore ternario (?:): selezione compatta inline per assegnazioni condizionali

In questa lezione, esploreremo ogni aspetto dei costrutti di selezione in profondità. Non ci limiteremo alla sintassi base, ma analizzeremo i pattern professionali, le best practices consolidate dall'esperienza di decenni di programmazione in C, gli errori comuni e come evitarli, le ottimizzazioni che i compilatori possono applicare, e i casi complessi che incontrerai nel codice reale. Imparerai non solo come usare questi costrutti, ma anche quando usarli, perché preferire uno rispetto all'altro, e come scrivere codice condizionale che sia robusto, leggibile e manutenibile.

Preparati per un viaggio completo nel mondo del controllo del flusso: dalla valutazione booleana alle condizioni complesse, dalla short-circuit evaluation agli statement annidati, dalle guard clauses ai lookup tables. Al termine di questa lezione, sarai in grado di scrivere logica condizionale come un vero professionista.

1. Il Costrutto if: La Base della Selezione

1.1 Sintassi Fondamentale e Semantica

Il costrutto if è il pilastro fondamentale di ogni tipo di selezione in C. La sua logica è semplice ma potente: "se questa condizione è vera, esegui questo blocco di codice; altrimenti, saltalo". Questa semplicità concettuale nasconde alcune sottigliezze importanti che ogni programmatore deve comprendere a fondo.

// Sintassi base del costrutto if
if (condizione) {
    // Blocco di codice eseguito solo se condizione è vera (non zero)
    istruzione1;
    istruzione2;
}

// Esempio concreto: verifica maggiore età
int eta = 20;

if (eta >= 18) {
    printf("Sei maggiorenne\n");
    printf("Puoi votare\n");
}

// L'esecuzione continua qui indipendentemente dalla condizione
printf("Fine del programma\n");

La condizione tra parentesi è un'espressione che viene valutata come valore numerico. In C, non esiste un vero tipo booleano nativo (fino a C99, che introduce _Bool e il header <stdbool.h>). La regola è semplice ma fondamentale:

🔍 Valutazione delle Condizioni in C:
  • Falso: Un valore è considerato "falso" se è esattamente zero (0, 0.0, NULL, '\0', ecc.)
  • Vero: Qualsiasi valore diverso da zero è considerato "vero" (1, -1, 42, 3.14, qualsiasi puntatore non NULL, ecc.)

Questo significa che puoi usare qualsiasi espressione che produca un valore numerico come condizione:

int x = 5;

if (x) {  // Vero perché x = 5 (diverso da zero)
    printf("x è diverso da zero\n");
}

if (x - 5) {  // Falso perché x - 5 = 0
    printf("Questo non viene stampato\n");
}

int *ptr = &x;
if (ptr) {  // Vero perché ptr non è NULL
    printf("Il puntatore è valido\n");
}

char c = 'A';
if (c) {  // Vero perché 'A' ha valore ASCII 65
    printf("Il carattere non è il null terminator\n");
}

1.2 Blocchi di Codice: Parentesi Graffe e Best Practices

Una delle decisioni stilistiche più dibattute nella programmazione C riguarda l'uso delle parentesi graffe con gli statement if. Tecnicamente, se il blocco contiene una sola istruzione, le graffe sono opzionali. Tuttavia, questa "comodità" sintattica è stata fonte di innumerevoli bug nel corso della storia della programmazione.

✗ Senza Graffe (SCONSIGLIATO)

// Tecnicamente valido ma PERICOLOSO
if (eta >= 18)
    printf("Maggiorenne\n");

// PERICOLO: se aggiungi un'istruzione dopo...
if (eta >= 18)
    printf("Maggiorenne\n");
    printf("Puoi votare\n");  // NON è dentro l'if!
    // Questo viene eseguito SEMPRE

// PROBLEMA con preprocessore
if (debug)
    #ifdef VERBOSE
    printf("Debug info\n");
    #endif
// Il preprocessore può causare comportamenti inattesi

Problemi: Bug difficili da individuare, comportamento inatteso quando si aggiunge codice, problemi con macro e preprocessore.

✓ Con Graffe (RACCOMANDATO)

// SEMPRE usa le graffe - best practice professionale
if (eta >= 18) {
    printf("Maggiorenne\n");
}

// Chiaro e sicuro quando aggiungi codice
if (eta >= 18) {
    printf("Maggiorenne\n");
    printf("Puoi votare\n");  // Chiaramente dentro l'if
}

// Nessun problema con preprocessore
if (debug) {
    #ifdef VERBOSE
    printf("Debug info\n");
    #endif
}

Vantaggi: Codice più sicuro, facile da modificare, meno propenso a errori, più leggibile per tutti.

💥 Il Caso Apple "goto fail" - Un Bug da 1 Miliardo di Dollari

Nel 2014, Apple ha scoperto un bug critico di sicurezza nel suo SSL/TLS implementation causato proprio dall'assenza di parentesi graffe. Il codice (semplificato) era:

if ((err = SSLVerifySignedServerKeyExchange(...)) != 0)
    goto fail;
    goto fail;  // SEMPRE eseguito! Bug critico!

// Altre verifiche che venivano saltate...

fail:
    return err;

Il secondo goto fail era al di fuori dell'if (per mancanza di graffe) e veniva sempre eseguito, saltando cruciali controlli di sicurezza SSL/TLS. Questo permetteva attacchi man-in-the-middle. Con le graffe, il bug sarebbe stato ovvio:

if ((err = SSLVerifySignedServerKeyExchange(...)) != 0) {
    goto fail;
    goto fail;  // Chiaramente ridondante - rilevato subito
}

Lezione: Usa SEMPRE le graffe. Non è solo stile, è sicurezza e professionalità.

1.3 Operatori di Confronto e Condizioni

Le condizioni negli statement if sono tipicamente costruite usando operatori di confronto e operatori logici. Comprendere a fondo questi operatori e il loro comportamento è essenziale per scrivere logica condizionale corretta.

Operatore Significato Esempio Note
== Uguale a if (x == 5) Attenzione a non confondere con = (assegnamento)
!= Diverso da if (x != 0) Equivalente a if (x) ma più esplicito
< Minore di if (x < 10) Confronto stretto (esclude il valore limite)
> Maggiore di if (x > 0) Confronto stretto
<= Minore o uguale if (x <= 100) Include il valore limite
>= Maggiore o uguale if (x >= 18) Include il valore limite

Operatori Logici per Condizioni Complesse

Spesso le decisioni dipendono da multiple condizioni combinate insieme. Gli operatori logici permettono di costruire espressioni booleane complesse:

Operatore Nome Esempio Vero quando
&& AND logico if (x > 0 && x < 10) Entrambe le condizioni sono vere
|| OR logico if (x < 0 || x > 10) Almeno una condizione è vera
! NOT logico if (!trovato) La condizione è falsa
// Esempi pratici di condizioni con operatori logici

// AND logico (&&) - ENTRAMBE le condizioni devono essere vere
int eta = 25;
int patente = 1;  // 1 = ha la patente, 0 = non ha la patente

if (eta >= 18 && patente) {
    printf("Puoi guidare\n");
}

// OR logico (||) - ALMENO UNA condizione deve essere vera
char tipo_utente = 'A';  // 'A' = admin, 'M' = moderatore

if (tipo_utente == 'A' || tipo_utente == 'M') {
    printf("Hai privilegi elevati\n");
}

// NOT logico (!) - inverte il valore di verità
int errore = 0;

if (!errore) {  // Equivalente a: if (errore == 0)
    printf("Operazione completata con successo\n");
}

// Combinazione di operatori logici
int voto = 85;
int presenza = 90;  // percentuale

if ((voto >= 60 && presenza >= 75) || voto >= 90) {
    printf("Esame superato\n");
}
// Passa se: (voto >= 60 E presenza >= 75%) OPPURE voto >= 90
// Le parentesi rendono esplicita la precedenza

// Precedenza degli operatori (dal più alto al più basso):
// 1. ! (NOT)
// 2. Operatori di confronto (<, >, <=, >=, ==, !=)
// 3. && (AND)
// 4. || (OR)


// Esempio di precedenza
if (!trovato && ricerca_completa || errore) {
    // Interpretato come: ((!trovato) && ricerca_completa) || errore
}

// Per chiarezza, usa sempre parentesi esplicite quando mischi operatori
if ((!trovato && ricerca_completa) || errore) {
    // Molto più chiaro!
}
⚠️ Short-Circuit Evaluation: Comportamento Cruciale da Conoscere

Gli operatori logici && e || in C utilizzano la short-circuit evaluation (valutazione in corto circuito). Questo significa che non tutte le sotto-espressioni vengono necessariamente valutate:

  • AND (&&): Se la prima condizione è falsa, la seconda NON viene valutata (perché il risultato sarà falso comunque)
  • OR (||): Se la prima condizione è vera, la seconda NON viene valutata (perché il risultato sarà vero comunque)
// Esempio di short-circuit con divisione
int x = 0, y = 10;

if (x != 0 && y / x > 2) {
    // Sicuro! Se x == 0, la seconda parte (y/x) non viene valutata
    // Evita divisione per zero
}

// Ordine delle condizioni IMPORTANTE
int *ptr = NULL;

// SBAGLIATO - crash!
if (*ptr == 5 && ptr != NULL) {
    // Tenta di dereferenziare ptr prima di controllare se è NULL
    // Causa segmentation fault!
}

// CORRETTO
if (ptr != NULL && *ptr == 5) {
    // Prima controlla se ptr è valido
    // Solo se ptr != NULL, valuta *ptr == 5
    // Sicuro!
}

// Short-circuit con funzioni che hanno side effects
int incrementa(int *n) {
    (*n)++;
    return *n;
}

int contatore = 0;

// ATTENZIONE: comportamento dipende da short-circuit
if (contatore > 5 && incrementa(&contatore) > 10) {
    // incrementa() viene chiamata SOLO se contatore > 5
}

if (contatore <= 5 || incrementa(&contatore) > 10) {
    // incrementa() viene chiamata SOLO se contatore > 5
}

Best Practice: Sfrutta il short-circuit per evitare errori (come divisione per zero o dereferenziazione di puntatori NULL), ma evita di fare affidamento su di esso per side effects complessi che potrebbero confondere chi legge il codice.

2. Il Costrutto if-else: Scelta Binaria

2.1 Sintassi e Semantica dell'if-else

Mentre if permette di eseguire codice quando una condizione è vera, spesso abbiamo bisogno di specificare esplicitamente cosa fare quando la condizione è falsa. L'else fornisce questa alternativa, creando una scelta binaria: "fai questo OPPURE fai quest'altro".

// Sintassi if-else
if (condizione) {
    // Blocco eseguito se condizione è VERA
    istruzioni_se_vero;
} else {
    // Blocco eseguito se condizione è FALSA
    istruzioni_se_falso;
}

// Esempio: determinare parità di un numero
int numero = 7;

if (numero % 2 == 0) {
    printf("%d è pari\n", numero);
} else {
    printf("%d è dispari\n", numero);
}

// Esempio: validazione input
int eta;
printf("Inserisci la tua età: ");
scanf("%d", &eta);

if (eta >= 0 && eta <= 120) {
    printf("Età valida: %d anni\n", eta);
} else {
    printf("ERRORE: Età non valida\n");
}

// Esempio: controllo accesso
char password[50];
printf("Password: ");
scanf("%49s", password);

if (strcmp(password, "secret123") == 0) {
    printf("Accesso consentito\n");
    // Codice per accesso riuscito...
} else {
    printf("Accesso negato\n");
    // Codice per gestire accesso fallito...
}

Diagramma di Flusso: if-else

Inizio
Condizione
è Vera?
✓ Sì
Blocco if
Esegui codice
✗ No
Blocco else
Esegui codice
Continua esecuzione

Funzionamento: Il programma valuta la condizione. Se è vera (diversa da zero), esegue il blocco if e salta il blocco else. Se è falsa (uguale a zero), salta il blocco if ed esegue il blocco else. In ogni caso, dopo aver eseguito uno dei due blocchi, l'esecuzione continua normalmente.

2.2 Il Pattern else-if: Selezione Multipla Sequenziale

Quando abbiamo più di due alternative da gestire, possiamo concatenare multiple condizioni usando il pattern else if. Questo crea una catena di controlli che vengono valutati in sequenza fino a trovare la prima condizione vera.

// Pattern else-if per selezione multipla
if (condizione1) {
    // Eseguito se condizione1 è vera
} else if (condizione2) {
    // Eseguito se condizione1 è falsa E condizione2 è vera
} else if (condizione3) {
    // Eseguito se condizione1 e 2 sono false E condizione3 è vera
} else {
    // Eseguito se TUTTE le condizioni precedenti sono false
}

// Esempio 1: Sistema di voti letterali
int voto = 85;

if (voto >= 90) {
    printf("Voto: A (Eccellente)\n");
} else if (voto >= 80) {
    printf("Voto: B (Ottimo)\n");
} else if (voto >= 70) {
    printf("Voto: C (Buono)\n");
} else if (voto >= 60) {
    printf("Voto: D (Sufficiente)\n");
} else {
    printf("Voto: F (Insufficiente)\n");
}

// Esempio 2: Calcolatrice semplice
char operatore;
double num1, num2, risultato;

printf("Inserisci operazione (es: 5 + 3): ");
scanf("%lf %c %lf", &num1, &operatore, &num2);

if (operatore == '+') {
    risultato = num1 + num2;
    printf("%.2f + %.2f = %.2f\n", num1, num2, risultato);
} else if (operatore == '-') {
    risultato = num1 - num2;
    printf("%.2f - %.2f = %.2f\n", num1, num2, risultato);
} else if (operatore == '*') {
    risultato = num1 * num2;
    printf("%.2f * %.2f = %.2f\n", num1, num2, risultato);
} else if (operatore == '/') {
    if (num2 != 0) {
        risultato = num1 / num2;
        printf("%.2f / %.2f = %.2f\n", num1, num2, risultato);
    } else {
        printf("ERRORE: Divisione per zero\n");
    }
} else {
    printf("ERRORE: Operatore '%c' non riconosciuto\n", operatore);
}

// Esempio 3: Categorizzazione età
int eta = 35;

if (eta < 0) {
    printf("Età non valida\n");
} else if (eta < 13) {
    printf("Bambino\n");
} else if (eta < 20) {
    printf("Adolescente\n");
} else if (eta < 65) {
    printf("Adulto\n");
} else if (eta <= 120) {
    printf("Senior\n");
} else {
    printf("Età non valida\n");
}
💡 Importante: Ordine delle Condizioni

Nel pattern else-if, l'ordine delle condizioni è critico. Le condizioni vengono valutate sequenzialmente dall'alto verso il basso, e si ferma alla prima che risulta vera. Le condizioni successive non vengono nemmeno valutate.

✗ Ordine Sbagliato

int voto = 95;

// SBAGLIATO!
if (voto >= 60) {
    printf("D\n");  // Viene eseguito!
} else if (voto >= 70) {
    printf("C\n");  // Mai raggiunto
} else if (voto >= 80) {
    printf("B\n");  // Mai raggiunto
} else if (voto >= 90) {
    printf("A\n");  // Mai raggiunto
}
// 95 >= 60 è vero, quindi stampa "D"
// e ignora tutte le altre condizioni!

✓ Ordine Corretto

int voto = 95;

// CORRETTO!
if (voto >= 90) {
    printf("A\n");  // Eseguito! Corretto
} else if (voto >= 80) {
    printf("B\n");
} else if (voto >= 70) {
    printf("C\n");
} else if (voto >= 60) {
    printf("D\n");
}
// Controlla dalla condizione più restrittiva
// alla meno restrittiva

Regola generale: Ordina le condizioni dalla più specifica/restrittiva alla più generale, o dalla più grande alla più piccola (per confronti numerici).

3. Il Costrutto switch-case: Selezione Multipla Basata su Valori

3.1 Sintassi e Funzionamento del switch

Quando devi fare una selezione tra molte alternative basate sul valore di una singola espressione intera, lo statement switch può essere più chiaro e potenzialmente più efficiente rispetto a una lunga catena di else-if. Il switch valuta un'espressione una sola volta e poi salta direttamente al case corrispondente.

// Sintassi base del switch
switch (espressione) {
    case valore1:
        // Codice eseguito se espressione == valore1
        istruzioni;
        break;  // Importante! Esce dallo switch
    
    case valore2:
        // Codice eseguito se espressione == valore2
        istruzioni;
        break;
    
    case valore3:
    case valore4:
        // Codice eseguito se espressione == valore3 O valore4
        istruzioni;
        break;
    
    default:
        // Eseguito se nessun case corrisponde (opzionale ma raccomandato)
        istruzioni;
        break;
}

// Esempio 1: Menu con switch
int scelta;

printf("=== MENU ===\n");
printf("1. Nuovo gioco\n");
printf("2. Carica partita\n");
printf("3. Opzioni\n");
printf("4. Esci\n");
printf("Scelta: ");
scanf("%d", &scelta);

switch (scelta) {
    case 1:
        printf("Avvio nuovo gioco...\n");
        // Codice per nuovo gioco
        break;
    
    case 2:
        printf("Caricamento partita salvata...\n");
        // Codice per caricare
        break;
    
    case 3:
        printf("Apertura menu opzioni...\n");
        // Codice per opzioni
        break;
    
    case 4:
        printf("Uscita dal gioco. Arrivederci!\n");
        exit(0);
        break;
    
    default:
        printf("ERRORE: Scelta non valida. Riprova.\n");
        break;
}

// Esempio 2: Giorni della settimana (case multipli)
int giorno;
printf("Inserisci numero giorno (1-7): ");
scanf("%d", &giorno);

switch (giorno) {
    case 1:
        printf("Lunedì - Inizio settimana\n");
        break;
    
    case 2:
    case 3:
    case 4:
        printf("Giorno feriale\n");
        break;
    
    case 5:
        printf("Venerdì - Quasi weekend!\n");
        break;
    
    case 6:
    case 7:
        printf("Weekend! 🎉\n");
        break;
    
    default:
        printf("Giorno non valido\n");
        break;
}

// Esempio 3: Operazioni con caratteri
char comando;
printf("Comando [S]alva [C]arica [E]sci: ");
scanf(" %c", &comando);  // Nota lo spazio prima di %c

switch (comando) {
    case 'S':
    case 's':
        printf("Salvataggio in corso...\n");
        break;
    
    case 'C':
    case 'c':
        printf("Caricamento in corso...\n");
        break;
    
    case 'E':
    case 'e':
        printf("Uscita...\n");
        break;
    
    default:
        printf("Comando non riconosciuto\n");
        break;
}

3.2 Il Comportamento del break e il Fall-Through

Una delle caratteristiche più importanti (e fonte di bug se non compresa) del switch è il comportamento di fall-through. Senza lo statement break, l'esecuzione "cade attraverso" al case successivo, eseguendo anche il suo codice.

⚠️ PERICOLO: Fall-Through Accidentale

Dimenticare il break è uno degli errori più comuni con lo switch e può causare comportamenti molto confusi:

// SBAGLIATO - break mancante causa fall-through non intenzionale
int livello = 2;

switch (livello) {
    case 1:
        printf("Livello Facile\n");
        // MANCA break! Cade al caso successivo
    
    case 2:
        printf("Livello Medio\n");
        // MANCA break! Cade al caso successivo
    
    case 3:
        printf("Livello Difficile\n");
        break;
}

// Output per livello = 2:
// Livello Medio
// Livello Difficile
// 
// NON è quello che volevamo!

// CORRETTO - con break
switch (livello) {
    case 1:
        printf("Livello Facile\n");
        break;  // Esce dallo switch
    
    case 2:
        printf("Livello Medio\n");
        break;
    
    case 3:
        printf("Livello Difficile\n");
        break;
}

Tuttavia, il fall-through può essere usato intenzionalmente per creare comportamenti utili. Quando lo fai intenzionalmente, è una buona pratica commentarlo esplicitamente:

// USO INTENZIONALE del fall-through (con commento esplicativo)
int mese = 2;  // Febbraio
int anno = 2024;
int giorni;

switch (mese) {
    case 1:  // Gennaio
    case 3:  // Marzo
    case 5:  // Maggio
    case 7:  // Luglio
    case 8:  // Agosto
    case 10: // Ottobre
    case 12: // Dicembre
        giorni = 31;
        break;
    
    case 4:  // Aprile
    case 6:  // Giugno
    case 9:  // Settembre
    case 11: // Novembre
        giorni = 30;
        break;
    
    case 2:  // Febbraio
        // Calcola anno bisestile
        if ((anno % 4 == 0 && anno % 100 != 0) || (anno % 400 == 0)) {
            giorni = 29;  // Anno bisestile
        } else {
            giorni = 28;  // Anno normale
        }
        break;
    
    default:
        printf("Mese non valido\n");
        giorni = 0;
        break;
}

printf("Il mese %d ha %d giorni\n", mese, giorni);

// Esempio con fall-through commentato esplicitamente
char vocale;
scanf(" %c", &vocale);

switch (vocale) {
    case 'a':
    case 'A':
        /* FALL THROUGH - intenzionale per gestire maiuscole e minuscole */
    case 'e':
    case 'E':
        /* FALL THROUGH */
    case 'i':
    case 'I':
        /* FALL THROUGH */
    case 'o':
    case 'O':
        /* FALL THROUGH */
    case 'u':
    case 'U':
        printf("È una vocale\n");
        break;
    
    default:
        printf("Non è una vocale\n");
        break;
}
Best Practices per il switch:
  1. Usa sempre break: A meno che il fall-through non sia intenzionale
  2. Commenta il fall-through: Se intenzionale, aggiungi un commento esplicito come /* FALL THROUGH */
  3. Includi sempre default: Anche se pensi di aver coperto tutti i casi, includi un case default per gestire valori inattesi
  4. Dichiara variabili fuori dal switch: Non dichiarare variabili dentro i case (a meno che non usi scope con graffe)
  5. Mantieni i case semplici: Se un case diventa troppo complesso, considera di estrarlo in una funzione separata

3.3 Limitazioni del switch e Quando NON Usarlo

Il switch ha alcune limitazioni importanti che devi conoscere. Non è uno strumento universale per ogni tipo di selezione multipla.

⚠️ Limitazioni del switch in C:
  • Solo tipi interi: L'espressione del switch deve essere un tipo intero (int, char, short, long, enum). NON puoi usare float, double, string, o puntatori.
    // ILLEGALE - non compila!
    char *nome = "Mario";
    switch (nome) {  // ERRORE: nome è un puntatore
        case "Mario":  // ERRORE: stringa non permessa
            // ...
    }
    
    // Usa if-else con strcmp per stringhe
    if (strcmp(nome, "Mario") == 0) {
        // ...
    } else if (strcmp(nome, "Luigi") == 0) {
        // ...
    }
    
  • Solo valori costanti: I valori nei case devono essere costanti note al momento della compilazione. Non puoi usare variabili.
    int max = 100;
    int x = 50;
    
    switch (x) {
        case max:  // ERRORE: max è una variabile, non una costante
            // ...
            break;
    }
    
    // Devi usare una costante
    #define MAX 100
    switch (x) {
        case MAX:  // OK: MAX è una costante preprocessor
            // ...
            break;
    }
    
  • Nessun range: Non puoi specificare range di valori direttamente. Devi elencare ogni valore o usare fall-through.
    // NON puoi fare (sintassi non valida in C standard):
    switch (voto) {
        case 90...100:  // NON standard (funziona in GCC come estensione)
            printf("A\n");
            break;
    }
    
    // Usa if-else per range
    if (voto >= 90 && voto <= 100) {
        printf("A\n");
    } else if (voto >= 80) {
        printf("B\n");
    }
    

3.4 switch vs if-else: Quando Usare Cosa?

Usa switch quando:

  • Hai molte alternative basate su un singolo valore intero o char
  • I valori sono costanti note (literals o #define)
  • Vuoi codice più pulito per menu o state machines
  • La selezione è basata su uguaglianza esatta (==)
Esempio ideale:
switch (command) {
    case 'w': move_up(); break;
    case 's': move_down(); break;
    case 'a': move_left(); break;
    case 'd': move_right(); break;
}

Usa if-else quando:

  • Devi confrontare range di valori (x > 10 && x < 20)
  • Le condizioni sono complesse o combinate
  • Lavori con float, double, o stringhe
  • Le condizioni non sono mutuamente esclusive
  • Hai poche alternative (2-3)
Esempio ideale:
if (temp > 30) {
    printf("Caldo\n");
} else if (temp > 15) {
    printf("Temperato\n");
} else {
    printf("Freddo\n");
}

4. L'Operatore Ternario (?:): Selezione Compatta

4.1 Sintassi e Utilizzo Base

L'operatore ternario è l'unico operatore in C che prende tre operandi. È un modo compatto per scrivere semplici selezioni if-else, particolarmente utile per assegnazioni condizionali inline.

// Sintassi: condizione ? valore_se_vero : valore_se_falso

// Esempio base: trovare il massimo
int a = 10, b = 20;
int max = (a > b) ? a : b;  // max = 20

// Equivalente a:
int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

// Esempio: parità
int numero = 7;
char *parita = (numero % 2 == 0) ? "pari" : "dispari";
printf("%d è %s\n", numero, parita);

// Uso inline in printf
int eta = 20;
printf("Sei %s\n", (eta >= 18) ? "maggiorenne" : "minorenne");

// Assegnamento condizionale
int sconto = (totale > 100) ? 10 : 0;

// Calcoli condizionali
int punti = 85;
char voto = (punti >= 90) ? 'A' : 
             (punti >= 80) ? 'B' :
             (punti >= 70) ? 'C' : 'F';

// Evitare divisione per zero
int divisore = 0;
int risultato = (divisore != 0) ? (100 / divisore) : 0;

4.2 Quando Usare (e Quando NON Usare) l'Operatore Ternario

✓ Usa l'operatore ternario per:

  • Assegnazioni semplici: Quando assegni uno di due valori in base a una condizione
  • Valori di ritorno: return (x > 0) ? x : -x;
  • Argomenti di funzione: printf("%d", (a > b) ? a : b);
  • Espressioni brevi e chiare: Quando rende il codice più conciso senza perdere chiarezza
// BUON uso
int abs_val = (x < 0) ? -x : x;
const char *status = connected ? "Online" : "Offline";
return (count > 0) ? count : 1;

✗ NON usare l'operatore ternario per:

  • Logica complessa: Con condizioni multiple o annidate profondamente
  • Side effects: Quando le espressioni modificano lo stato
  • Codice poco chiaro: Se rende il codice meno leggibile
  • Blocchi di codice: Quando serve eseguire multiple istruzioni
// CATTIVO uso - troppo complesso!
int result = (a > b) ? ((c > d) ? 
              ((e > f) ? x : y) : z) : w;
// Impossibile da leggere!

// MEGLIO con if-else
int result;
if (a > b) {
    if (c > d) {
        result = (e > f) ? x : y;
    } else {
        result = z;
    }
} else {
    result = w;
}
Regola d'Oro per l'Operatore Ternario:

Se il tuo operatore ternario non sta su una sola riga (o al massimo due) in modo leggibile, o se devi pensarci più di 2 secondi per capirlo, usa un if-else. La leggibilità è sempre più importante della brevità.

// ✓ Buono - chiaro e conciso
int max = (a > b) ? a : b;

// ✗ Cattivo - troppo su una riga
int x = (a>b)?(c>d)?(e>f)?g:h:i:j;

// ✓ Se proprio devi annidare, usa indentazione chiara
int grade = (score >= 90) ? 'A' :
            (score >= 80) ? 'B' :
            (score >= 70) ? 'C' :
            (score >= 60) ? 'D' : 'F';
// Accettabile perché è un pattern chiaro e leggibile

5. Pattern Avanzati e Tecniche Professionali

5.1 Guard Clauses: Early Return per Codice Più Chiaro

Una delle tecniche più efficaci per scrivere codice condizionale leggibile è l'uso delle guard clauses (clausole di guardia). Questa tecnica rappresenta un cambio di paradigma rispetto all'approccio tradizionale di annidamento degli if. L'idea fondamentale è semplice ma potente: invece di costruire una piramide di condizioni annidate dove il "percorso felice" (happy path) del codice si trova sepolto in profondità, gestiamo immediatamente i casi eccezionali, le pre-condizioni non soddisfatte e gli errori all'inizio della funzione, terminando l'esecuzione con un return anticipato.

Questo approccio porta numerosi vantaggi concreti e misurabili. Prima di tutto, riduce drasticamente la complessità cognitiva: quando leggi una funzione con guard clauses, sai immediatamente quali sono tutte le condizioni che devono essere soddisfatte per procedere. Non devi mentalmente tenere traccia di multipli livelli di annidamento o ricordarti di chiudere le parentesi graffe nel posto giusto. Inoltre, il codice principale - quello che fa effettivamente il lavoro della funzione - rimane al livello di indentazione base, rendendolo immediatamente visibile e comprensibile.

Dal punto di vista della manutenibilità, le guard clauses sono superiori perché ogni controllo è isolato e indipendente. Se devi aggiungere una nuova validazione o modificarne una esistente, sai esattamente dove intervenire senza dover riorganizzare l'intera struttura di controllo. Questo pattern è particolarmente apprezzato in progetti grandi dove il codice viene letto e modificato da team diversi nel tempo. È anche un pattern che i code reviewer riconoscono immediatamente come segno di codice maturo e professionale.

Vediamo ora un confronto pratico e dettagliato tra l'approccio tradizionale con annidamento profondo e l'uso delle guard clauses:

✗ Annidamento Profondo

int process_user(int user_id, char *data) {
    if (user_id > 0) {
        if (data != NULL) {
            if (strlen(data) > 0) {
                if (validate_data(data)) {
                    // Finalmente il codice utile
                    // Ma è annidato 4 livelli!
                    return save_to_db(user_id, data);
                } else {
                    return -4;
                }
            } else {
                return -3;
            }
        } else {
            return -2;
        }
    } else {
        return -1;
    }
}

Difficile da leggere, troppi livelli di annidamento, mental overhead elevato

✓ Guard Clauses (Early Return)

int process_user(int user_id, char *data) {
    // Guard clauses: gestisci i casi eccezionali subito
    if (user_id <= 0) {
        return -1;  // User ID invalido
    }
    
    if (data == NULL) {
        return -2;  // Data è NULL
    }
    
    if (strlen(data) == 0) {
        return -3;  // Data vuoto
    }
    
    if (!validate_data(data)) {
        return -4;  // Validazione fallita
    }
    
    // Codice "happy path" al livello base - molto leggibile!
    return save_to_db(user_id, data);
}

Chiaro, lineare, facile da leggere, ogni controllo è esplicito

5.2 Lookup Tables: Evitare Lunghe Catene di if-else

Quando ti trovi a scrivere lunghe catene di if-else o switch giganteschi che essenzialmente mappano valori di input a valori di output in modo diretto e deterministico, è il momento di considerare una lookup table (tabella di ricerca). Una lookup table è fondamentalmente un array (o una struttura dati simile) che memorizza i risultati pre-calcolati per tutte le possibili combinazioni di input. Invece di eseguire una serie di confronti a runtime, il programma semplicemente indicizza la tabella e recupera il risultato in tempo costante O(1).

I vantaggi delle lookup tables sono molteplici e significativi. Dal punto di vista delle prestazioni, eliminano completamente la necessità di eseguire confronti condizionali multipli. Mentre una catena di if-else o uno switch potrebbero richiedere fino a N confronti nel caso peggiore (dove N è il numero di casi), una lookup table fornisce sempre l'accesso in tempo costante. Questo può fare una differenza enorme in codice che viene eseguito frequentemente, come parser, interpreti, o processori di dati in tempo reale.

Ma i benefici non sono solo prestazionali. Le lookup tables migliorano drasticamente la manutenibilità del codice. Quando i dati sono separati dalla logica di controllo, diventa molto più facile visualizzare, verificare e modificare le mappature. Puoi vedere immediatamente l'intera tabella di corrispondenze in un colpo d'occhio, senza dover scorrere pagine di if-else. Se devi aggiungere un nuovo caso, è una questione di aggiungere un elemento all'array, non di inserire un nuovo branch condizionale nel posto giusto della catena.

Inoltre, le lookup tables rendono il codice più testabile. Puoi facilmente scrivere test che verificano l'intera tabella iterando su tutti gli input possibili, cosa che sarebbe molto più verbosa con if-else. Puoi anche caricare le tabelle da file di configurazione esterni, permettendo di modificare il comportamento del programma senza ricompilazione.

Naturalmente, le lookup tables non sono sempre la soluzione migliore. Richiedono memoria per memorizzare la tabella (che potrebbe essere significativa per domini di input grandi), e funzionano meglio quando:

Vediamo ora alcuni esempi concreti che dimostrano la potenza di questo pattern:

Esempio: Conversione Esadecimale senza Lookup Table

// Approccio con if-else (lungo e ripetitivo)
int hex_to_decimal_verbose(char hex) {
    if (hex == '0') return 0;
    if (hex == '1') return 1;
    if (hex == '2') return 2;
    // ... 16 controlli in totale!
    if (hex == 'F' || hex == 'f') return 15;
    return -1;  // Errore
}

// Con lookup table (elegante e veloce!)
int hex_to_decimal(char hex) {
    // Array indicizzato per valore ASCII
    static const int lookup[256] = {
        ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3,
        ['4'] = 4, ['5'] = 5, ['6'] = 6, ['7'] = 7,
        ['8'] = 8, ['9'] = 9,
        ['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13,
        ['E'] = 14, ['F'] = 15,
        ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13,
        ['e'] = 14, ['f'] = 15
    };
    
    int result = lookup[(unsigned char)hex];
    return (result || hex == '0') ? result : -1;
}

// Esempio: giorni nel mese (già visto ma ottimizzato)
const int DAYS_IN_MONTH[] = {
    31, 28, 31, 30, 31, 30,  // Gen-Giu
    31, 31, 30, 31, 30, 31   // Lug-Dic
};

int get_days_in_month(int month, int year) {
    if (month < 1 || month > 12) {
        return -1;  // Errore
    }
    
    int days = DAYS_IN_MONTH[month - 1];
    
    // Gestione febbraio per anno bisestile
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || 
                           (year % 400 == 0))) {
        days = 29;
    }
    
    return days;
}

5.3 Evitare Condizioni Duplicate: Il Principio DRY

DRY - "Don't Repeat Yourself" (Non Ripeterti) - è uno dei principi fondamentali della programmazione professionale, coniato da Andy Hunt e Dave Thomas nel loro celebre libro "The Pragmatic Programmer". Sebbene il principio si applichi a tutti gli aspetti dello sviluppo software, è particolarmente rilevante quando parliamo di logica condizionale. La ripetizione di condizioni identiche o molto simili in punti diversi del codice è un code smell - un sintomo che indica potenziali problemi nel design del software.

Quando duplichi una condizione, stai essenzialmente codificando la stessa regola di business o lo stesso constraint in multipli luoghi. Questo crea immediatamente diversi problemi concreti e misurabili. Il primo e più ovvio è la difficoltà di manutenzione: quando quella regola cambia (e cambierà - le requirements evolvono sempre), devi ricordarti di modificarla in tutti i posti dove l'hai scritta. Dimentichi anche solo un'occorrenza, e hai introdotto un bug subdolo dove parti diverse del programma applicano versioni diverse della stessa regola.

Il secondo problema è l'inconsistenza. Anche se inizialmente tutte le copie della condizione sono identiche, nel tempo tendono a divergere. Qualcuno modifica una copia per gestire un caso particolare, ma non applica la stessa modifica alle altre. Oppure durante un merge di codice, le modifiche vengono applicate solo ad alcune versioni. Il risultato è un comportamento inconsistente e imprevedibile del programma, dove la stessa condizione logica produce risultati diversi in contesti diversi.

Il terzo problema, spesso sottovalutato, è la leggibilità. Quando vedi la stessa condizione complessa ripetuta più volte, devi ogni volta re-parsarla mentalmente per capire cosa fa. Se invece quella condizione ha un nome descrittivo (tramite una funzione o una macro ben nominata), la comprensione è immediata. Il nome diventa una forma di documentazione auto-esplicativa.

La soluzione al problema della duplicazione è la centralizzazione attraverso l'astrazione. Ci sono diversi modi per farlo in C:

Un aspetto importante del principio DRY nel contesto delle condizioni è che non si tratta solo di eliminare duplicazione sintattica (lo stesso codice scritto due volte), ma anche duplicazione semantica (la stessa conoscenza espressa in modi diversi). Per esempio, age >= 18 e is_adult esprimono la stessa conoscenza, anche se sintatticamente diversi. Il secondo è preferibile perché centralizza la definizione di "essere adulto" e la rende modificabile in un solo punto.

Vediamo ora un esempio pratico e concreto di come il principio DRY si applica alle condizioni duplicate, con un confronto tra approccio non-DRY e approccio corretto:

✗ Condizioni Duplicate

if (user.age >= 18 && user.has_id && !user.banned) {
    allow_entry(user);
}

// Più avanti nel codice...
if (user.age >= 18 && user.has_id && !user.banned) {
    allow_purchase(user);
}

// Ancora più avanti...
if (user.age >= 18 && user.has_id && !user.banned) {
    allow_voting(user);
}

// Problema: stessa logica ripetuta 3 volte!
// Se cambia la regola, devi modificare 3 posti

✓ Condizione Centralizzata

// Funzione o macro per centralizzare la logica
int is_verified_adult(struct User user) {
    return user.age >= 18 && 
           user.has_id && 
           !user.banned;
}

// Ora usa la funzione ovunque
if (is_verified_adult(user)) {
    allow_entry(user);
}

if (is_verified_adult(user)) {
    allow_purchase(user);
}

if (is_verified_adult(user)) {
    allow_voting(user);
}

// Vantaggio: logica in un solo posto
// Manutenzione più facile
// Nome descrivivo migliora leggibilità

6. Errori Comuni e Come Evitarli: Imparare dai Classici Scivoloni

La storia della programmazione è costellata di bug causati da errori nelle condizioni. Alcuni di questi errori sono così comuni e pervasivi che ogni programmatore C li incontra almeno una volta nella sua carriera. La buona notizia è che conoscere questi pattern di errore ti permette di evitarli proattivamente e di riconoscerli immediatamente quando li vedi in code review o durante il debugging. In questa sezione, esploreremo i più insidiosi e frequenti errori legati ai costrutti di selezione, analizzando non solo cosa va storto, ma anche perché questi errori sono così facili da commettere e come le pratiche professionali possono eliminarli.

6.1 Confondere = con ==: L'Errore che Non Muore Mai

Se c'è un singolo errore che ha causato più bug, più ore di debugging frustrato, e più problemi di produzione nella storia del C, è probabilmente questo: confondere l'operatore di assegnamento = con l'operatore di confronto ==. La ragione per cui questo errore è così comune è puramente visuale: i due operatori differiscono di un solo carattere, e quando scorri rapidamente il codice, il cervello tende a "vedere" quello che si aspetta di vedere, non necessariamente quello che c'è realmente scritto.

Ma c'è di più. Il problema è aggravato dal fatto che in C, l'assegnamento è un'espressione, non uno statement. Questo significa che x = 5 non solo assegna 5 a x, ma restituisce anche il valore assegnato (5 in questo caso). Questa caratteristica, sebbene utile in certi contesti (come while ((c = getchar()) != EOF)), rende legale scrivere if (x = 5) dal punto di vista sintattico. Il compilatore non può sapere se hai intenzionalmente voluto fare un assegnamento nella condizione o se hai semplicemente dimenticato un carattere =.

💥 L'ERRORE PIÙ COMUNE: Assignment invece di Comparison

Questo è probabilmente l'errore più frequente e insidioso nella programmazione C. L'operatore di assegnamento = e l'operatore di confronto == si somigliano, ma hanno significati completamente diversi.

int x = 10;

// SBAGLIATO - usa = invece di ==
if (x = 5) {  // ASSEGNA 5 a x, poi valuta 5 (vero!)
    printf("Questo viene SEMPRE eseguito!\n");
    printf("x ora vale: %d\n", x);  // Stampa 5!
}
// x è stato modificato da 10 a 5!

// CORRETTO - usa ==
if (x == 5) {  // CONFRONTA x con 5
    printf("x è uguale a 5\n");
}

// Caso ancora più insidioso
if (x = 0) {  // Assegna 0 a x, 0 è FALSO
    printf("Mai eseguito\n");
}
// Ma x è stato azzerato!

// Tecnica difensiva: Yoda Conditions
if (5 == x) {  // Costante a sinistra
    // Se scrivi accidentalmente if (5 = x), è un errore di compilazione!
}

// Compilatori moderni con -Wall avvisano su if (x = 5)
// Ma non sempre, quindi attenzione!

6.2 Punto e Virgola dopo if

⚠️ Il Punto e Virgola Maledetto
// SBAGLIATO - punto e virgola dopo la condizione
int x = 10;

if (x > 5);  // ERRORE! Crea statement vuoto
{
    printf("Questo viene SEMPRE eseguito!\n");
}

// Equivalente a:
if (x > 5) {
    ;  // Statement vuoto - non fa nulla
}
{   // Blocco indipendente - sempre eseguito!
    printf("Questo viene SEMPRE eseguito!\n");
}

// CORRETTO - nessun punto e virgola
if (x > 5) {
    printf("Eseguito solo se x > 5\n");
}

6.3 Confondere Operatori Logici e Bitwise

⚠️ && vs & e || vs |
// && e || sono operatori LOGICI (short-circuit)
// & e | sono operatori BITWISE (sempre valutano entrambi)

int a = 1, b = 0;

// CORRETTO - operatori logici
if (a && b) {  // Falso perché b = 0
    printf("Non eseguito\n");
}

// SBAGLIATO (probabilmente) - operatore bitwise
if (a & b) {  // Bitwise AND: 1 & 0 = 0 (falso)
    printf("Non eseguito\n");
}
// Funziona per caso, ma è sbagliato concettualmente!

// Problema più chiaro:
int x = 2, y = 3;

if (x && y) {  // Logico: true (entrambi != 0)
    printf("Eseguito\n");
}

if (x & y) {  // Bitwise: 2 & 3 = 0010 & 0011 = 0010 = 2 (true)
    printf("Eseguito\n");
}

// Ma con valori diversi:
x = 4; y = 8;

if (x && y) {  // Logico: true
    printf("Eseguito\n");
}

if (x & y) {  // Bitwise: 0100 & 1000 = 0000 = 0 (false!)
    printf("NON eseguito!\n");
}

// REGOLA: usa SEMPRE && e || per condizioni logiche

7. Best Practices Professionali Complete

✓ Checklist delle Best Practices
  1. Usa sempre le graffe { } anche per blocch

    7.5 Checklist Completa delle Best Practices Professionali

    ✓ La Checklist Definitiva per Codice Condizionale Professionale

    Questa checklist racchiude tutte le best practices che abbiamo esplorato, più altre raccomandazioni importanti. Usa questa lista come riferimento durante la scrittura del codice e durante le code review.

    1. ✓ Usa sempre le graffe { } per tutti i blocchi condizionali, anche quelli a singola istruzione. Nessuna eccezione.
    2. ✓ Preferisci condizioni positive - scrivi if (is_valid) invece di if (!is_invalid). Evita doppie negazioni.
    3. ✓ Usa parentesi per chiarire la precedenza nelle condizioni complesse, anche quando tecnicamente non necessarie.
    4. ✓ Sostituisci i magic numbers con costanti nominate - usa #define o const con nomi descrittivi.
    5. ✓ Gestisci sempre il caso default negli switch con default: e considera l'else finale nelle catene if-else-if.
    6. ✓ Usa il break negli switch a meno che il fall-through non sia intenzionale, e in quel caso commentalo esplicitamente con /* FALL THROUGH */.
    7. ✓ Applica guard clauses per gestire casi speciali ed errori all'inizio delle funzioni con early return.
    8. ✓ Evita annidamenti profondi (>3 livelli) - se necessario, estrai la logica in funzioni separate.
    9. ✓ Commenta le condizioni complesse spiegando il "perché", non il "cosa". Il codice dovrebbe essere auto-documentante per il "cosa".
    10. ✓ Mantieni le funzioni corte - se una funzione ha troppa logica condizionale, è probabilmente un segno che dovrebbe essere scomposta.
    11. ✓ Centralizza le condizioni duplicate - applica il principio DRY estraendo la logica in funzioni o macro con nomi significativi.
    12. ✓ Testa i casi limite - zero, valori negativi, NULL, stringhe vuote, valori massimi/minimi, ecc.
    13. ✓ Usa == non = nelle condizioni - verifica sempre che stai confrontando, non assegnando. Abilita i warning del compilatore.
    14. ✓ Usa && e || per logica, non & e |. Gli operatori bitwise sono per manipolazione bit-a-bit, non per condizioni booleane.
    15. ✓ Considera lookup tables per lunghe catene di if-else o switch quando hai mappature semplici e dirette.
    16. ✓ Sfrutta la short-circuit evaluation mettendo condizioni "economiche" o più probabili prima nelle catene && e ||.
    17. ✓ Usa switch per valori discreti, if-else per range e condizioni complesse.
    18. ✓ L'operatore ternario solo per casi semplici - se non sta su una riga leggibile, usa if-else.
    19. ✓ Compila con warning abilitati - usa sempre -Wall -Wextra e tratta i warning come errori.
    20. ✓ Code review è essenziale - molti errori condizionali sono ovvi a una seconda coppia di occhi.

    Ricorda: Queste non sono solo regole arbitrarie o questioni di stile personale. Sono pratiche consolidate che prevengono bug reali, migliorano la manutenibilità, e rendono il tuo codice più professionale e affidabile. Seguirle distingue uno sviluppatore entry-level da uno senior.

">int a = 1, b = 0; // CORRETTO - operatori logici if (a && b) { // AND logico: vero solo se entrambi diversi da zero printf("Entrambi veri\n"); // Non eseguito: b è 0 } // SBAGLIATO (probabilmente) - operatore bitwise usato come logico if (a & b) { // AND bitwise: 0001 & 0000 = 0000 = 0 (falso) printf("Non eseguito\n"); } // In questo caso, per coincidenza, dà lo stesso risultato, // ma è concettualmente sbagliato e può fallire con altri valori! // Il problema diventa evidente con valori diversi: int x = 2, y = 3; // 0010 e 0011 in binario if (x && y) { // Logico: true (entrambi non zero) printf("LOGICO: Eseguito\n"); // Eseguito } if (x & y) { // Bitwise: 0010 & 0011 = 0010 = 2 (diverso da zero, quindi "vero") printf("BITWISE: Eseguito\n"); // Casualmente anche eseguito } // Ma cambiando i valori, il comportamento diverge: x = 4; y = 8; // 0100 e 1000 in binario if (x && y) { // Logico: true (entrambi non zero) printf("LOGICO: Eseguito\n"); // Eseguito correttamente } if (x & y) { // Bitwise: 0100 & 1000 = 0000 = 0 (falso!) printf("BITWISE: NON eseguito!\n"); // Non eseguito - BUG! } // PERICOLO con short-circuit evaluation int *ptr = NULL; // SICURO con && (short-circuit) if (ptr != NULL && *ptr == 5) { // Se ptr è NULL, *ptr non viene valutato - nessun crash } // PERICOLOSO con & (no short-circuit) if (ptr != NULL & *ptr == 5) { // CRASH! // Entrambe le espressioni vengono valutate SEMPRE // Se ptr è NULL, *ptr causa segmentation fault! } // Esempio con side effects int count = 0; int increment_and_check() { count++; return count > 5; } // Con && (short-circuit) if (0 && increment_and_check()) { // increment_and_check() NON viene chiamata (0 è falso) } // count resta 0 // Con & (sempre valuta entrambi) if (0 & increment_and_check()) { // increment_and_check() VIENE chiamata comunque! } // count diventa 1 - side effect inatteso! // REGOLA ORO: // Usa SEMPRE && e || per condizioni logiche // Usa & e | SOLO per manipolazione di bit o flag bitmask

Quando Usare Cosa:

6.4 Il Problema del Dangling Else

Il "dangling else" (else sospeso) è un problema di ambiguità sintattica che può causare confusione, specialmente quando si annidano statement if senza usare le graffe. Il problema nasce dal fatto che quando hai multipli if annidati seguiti da un else, non è immediatamente chiaro (almeno visivamente) a quale if appartenga quell'else.

Il C risolve questa ambiguità con una regola semplice: un else si associa sempre all'if più vicino che non ha ancora un else. Questa regola è logica e ben definita, ma può portare a codice che si comporta diversamente da come appare visivamente, specialmente se l'indentazione è fuorviante.

⚠️ Dangling Else: A Quale if Appartiene?
// AMBIGUO - a quale if appartiene l'else?
int a = 5, b = 10, c = 15;

if (a > 0)
    if (b > 0)
        c = 1;
else  // A quale if appartiene questo else?
    c = 2;

// L'indentazione suggerisce che else vada con il primo if,
// ma in realtà appartiene al secondo if (quello più vicino)!
// È equivalente a:

if (a > 0) {
    if (b > 0) {
        c = 1;
    } else {  // else del secondo if
        c = 2;
    }
}
// Se a <= 0, c non viene modificato (resta 15)

// Se volevi che else andasse con il primo if:
if (a > 0) {
    if (b > 0) {
        c = 1;
    }
} else {  // else del primo if - ora è chiaro!
    c = 2;
}

// SOLUZIONE: USA SEMPRE LE GRAFFE
// Rendono esplicita e visibile la struttura

Questo è un altro esempio di perché le graffe sono sempre raccomand ate, anche per statement singoli. Eliminano completamente l'ambiguità e rendono il codice immune a questo tipo di errore.

7. Best Practices Professionali: La Guida Definitiva

Scrivere codice condizionale che funziona è relativamente facile. Scrivere codice condizionale che sia robusto, manutenibile, leggibile e professionale richiede disciplina e l'applicazione coerente di best practices consolidate. Queste pratiche sono il risultato di decenni di esperienza collettiva della comunità di programmatori C, e seguirle ti distingue come uno sviluppatore maturo e professionale. In questa sezione, esploreremo non solo cosa fare, ma anche e soprattutto perché farlo.

7.1 La Regola d'Oro: Sempre le Graffe

✓ Best Practice #1: Usa SEMPRE le Graffe { }

Questa è probabilmente la singola pratica più importante e con il maggiore impatto sulla qualità del codice condizionale. Anche se il C permette di omettere le graffe per blocchi a singola istruzione, farlo apre la porta a una varietà di bug sottili e problematici. Analizziamo nel dettaglio perché questa pratica è così cruciale.

Prevenzione degli Errori di Manutenzione: Quando un blocco if ha una sola istruzione senza graffe, aggiungere una seconda istruzione è un'operazione apparentemente innocua che in realtà introduce un bug. Il programmatore che aggiunge la riga non sempre si accorge che deve anche aggiungere le graffe. Con le graffe sempre presenti, questo problema non può verificarsi - aggiungi semplicemente la nuova riga dentro il blocco esistente.

Chiarezza Visiva: Le graffe rendono immediatamente ovvio dove inizia e finisce un blocco condizionale. Non devi fare affidamento sull'indentazione (che potrebbe essere sbagliata o inconsistente) o contare gli statement. La struttura del codice è esplicita e inequivocabile.

Protezione dal Dangling Else: Come abbiamo visto nella sezione errori, l'else sospeso è molto meno ambiguo quando usi sempre le graffe. La struttura annidata diventa cristallina.

Compatibilità con Macro e Preprocessore: Le macro del preprocessore possono causare comportamenti inattesi con statement senza graffe. Le graffe proteggono dalla maggior parte di questi problemi.

Standard di Codice Professionale: Praticamente tutti gli standard di codifica professionale (Google C++ Style Guide, Linux Kernel Style, MISRA C, ecc.) richiedono o raccomandano fortemente l'uso di graffe. È un segno di codice maturo e ben mantenuto.

// ✓ SEMPRE così (anche per una sola istruzione)
if (condition) {
    do_something();
}

if (x > 0) {
    printf("Positivo\n");
}

while (running) {
    process_event();
}

// ✗ MAI così (anche se tecnicamente legale)
if (condition)
    do_something();

if (x > 0)
    printf("Positivo\n");

while (running)
    process_event();

7.2 Condizioni Positive: Leggibilità Prima di Tutto

Preferisci Condizioni Positive a Quelle Negative

Il cervello umano processa le affermazioni positive più facilmente delle negazioni. Quando leggi "se è valido, fai X", la comprensione è immediata. Quando leggi "se non è invalido, fai X", devi fare un passaggio mentale extra per elaborare la doppia negazione. Questo overhead cognitivo si accumula quando leggi centinaia di righe di codice.

Inoltre, le condizioni negative tendono a rendere il codice più difficile da modificare. Se la logica cambia, invertire una condizione negativa richiede più attenzione per assicurarsi che la semantica rimanga corretta.

// ✓ PREFERIBILE - condizione positiva
if (is_valid) {
    process_data();
} else {
    handle_error();
}

// ✗ MENO CHIARO - doppia negazione
if (!is_invalid) {
    process_data();
} else {
    handle_error();
}

// ✓ ANCORA MEGLIO - usa nomi booleani che leggono naturalmente
if (user_is_authenticated) {  // Legge come inglese naturale
    show_dashboard();
}

if (has_permission) {  // Chiaro e diretto
    allow_access();
}

7.3 Parentesi per la Chiarezza

Usa Parentesi per Rendere Esplicita la Precedenza

Anche se conosci perfettamente la precedenza degli operatori in C (e dovresti!), non tutti i lettori del tuo codice la ricordano perfettamente, specialmente nelle condizioni complesse che mixano operatori logici, di confronto e aritmetici. Le parentesi extra non costano nulla in termini di performance (il compilatore le ignora) ma migliorano enormemente la leggibilità.

Come regola empirica: se devi pensare anche solo un secondo per ricordare la precedenza, aggiungi le parentesi. Il codice viene letto molto più spesso di quanto viene scritto, quindi ogni secondo risparmiato al lettore è tempo ben investito.

// ✓ CHIARO - parentesi rendono esplicite le precedenze
if ((a > 0) && (b < 10)) {
    // Ovvio: a deve essere positivo E b minore di 10
}

if ((x == 5) || (y == 10)) {
    // Chiaro: uno O l'altro
}

if ((age >= 18) && (has_license || has_permit)) {
    // Esplicito: adulto E (ha patente O ha permesso)
}

// ✗ MENO CHIARO - devi conoscere/ricordare la precedenza
if (a > 0 && b < 10) {  // Funziona ma meno ovvio
}

if (age >= 18 && has_license || has_permit) {
    // Ambiguo! È (age >= 18 && has_license) || has_permit
    // oppure age >= 18 && (has_license || has_permit)?
    // Risposta: il primo, ma non è ovvio!
}

7.4 Evita Magic Numbers: Costanti Nominate

Sostituisci i Numeri Letterali con Costanti Nominate

I "magic numbers" - numeri letterali sparsi nel codice senza spiegazione - sono uno dei peggiori nemici della manutenibilità. Quando vedi if (age >= 18), il 18 sembra abbastanza ovvio nel contesto. Ma sei mesi dopo, o in un contesto diverso, o in un progetto che opera in giurisdizioni diverse, quel 18 potrebbe non essere più così ovvio. È l'età per votare? Per guidare? Per bere alcolici? Può variare per legge?

Le costanti nominate risolvono multipli problemi simultaneamente: documentano l'intento (il nome spiega cosa significa quel numero), centralizzano i valori (se cambia, cambi in un solo posto), e rendono il codice più flessibile (puoi facilmente cambiare il valore o persino calcolarlo dinamicamente).

// ✗ MALE - magic numbers
if (age >= 18) {
    // 18 cosa? Per cosa?
}

if (speed > 120) {
    // 120 in km/h? mph? Limite urbano o autostradale?
}

if (temperature < 0) {
    // Celsius o Fahrenheit?
}

// ✓ BENE - costanti nominate chiare
#define LEGAL_ADULT_AGE 18
#define VOTING_AGE 18
#define HIGHWAY_SPEED_LIMIT_KMH 120
#define FREEZING_POINT_CELSIUS 0

if (age >= LEGAL_ADULT_AGE) {
    // Crystal clear!
}

if (speed_kmh > HIGHWAY_SPEED_LIMIT_KMH) {
    // Ovvio cosa stiamo verificando
}

if (temperature_c < FREEZING_POINT_CELSIUS) {
    // Nessun dubbio sull'unità di misura
}

// Oppure con const (C99+)
const int MAX_CONNECTIONS = 100;
const double PI = 3.14159265359;
const char *DEFAULT_USER = "guest";